package com.xrui.hbase;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.primitives.UnsignedBytes;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * This class represents an encapsulation of the parts of a HBase row key,
 * suitable for translation to an {@link EntityId}.
 * <p>
 * <p>RowKeyComponents has one factory method, {@link #fromComponents(Object...)}.</p>
 * <p>
 * <p>RowKeyComponents consist of an ordered series of <code>Object</code>s. There are
 * presently two valid forms of a RowKeyComponents:
 * <ul>
 * <li>A single component, either a <code>byte[]</code> or <code>String</code>. This
 * is suitable for generating raw EntityIds.</li>
 * <li>One or more components of types <code>String</code>, <code>Integer</code>,
 * or <code>Long</code>. This is suitable for generating formatted EntityIds.
 * May contain trailing nulls, but the leading hash components must not be null.</li>
 * </ul>
 * <p>
 * <p>For more information on these formats, see {@link EntityId}. There are two ways to convert
 * a RowKeyComponents to an EntityId:
 * <ul>
 * <li>Via the method {@link #getEntityIdForTable(Table)}</li>
 * </ul>
 * <p>
 */
public final class RowKeyComponents implements Comparable<RowKeyComponents> {
    /**
     * The backing array of components.
     */
    private Object[] mComponents;

    /**
     * Private constructor.
     *
     * @param components the components.
     */
    private RowKeyComponents(Object[] components) {
        mComponents = components;
    }

    /**
     * Creates a RowKeyComponents from components.
     *
     * @param components the components of the row key.
     * @return a RowKeyComponents
     */
    public static RowKeyComponents fromComponents(Object... components) {
        Preconditions.checkNotNull(components);
        Preconditions.checkArgument(components.length > 0);
        Preconditions.checkNotNull(components[0], "First component cannot be null.");

        // Some of these checks will be redundant when the RowKeyComponents is converted
        // into an EntityId, but putting them here helps the user see them at the time of creation.
        if (components[0] instanceof byte[]) {
            Preconditions.checkArgument(components.length == 1, "byte[] only valid as sole component.");
            // We need to make a deep copy of the byte[] to ensure that a later mutation to the original
            // byte[] doesn't confuse hash codes, etc.
            byte[] original = (byte[]) components[0];
            return new RowKeyComponents(new Object[]{Arrays.copyOf(original, original.length)});
        } else {
            boolean seenNull = false;
            for (Object component : components) {
                if (seenNull) {
                    Preconditions.checkArgument(
                        component == null,
                        "Row Keys cannot contain have a non-null component after a null component");
                } else {
                    if (component == null) {
                        seenNull = true;
                    } else {
                        Preconditions.checkArgument(
                            (component instanceof String) || (component instanceof Integer) || (component instanceof Long),
                            "Components must be of type String, Integer, or Long.");
                    }
                }
            }
        }

        // Pass in a copy rather than the actual array, just in case the user called us with an
        // Object[], which would make components mutable.
        return new RowKeyComponents(Arrays.copyOf(components, components.length));
    }

    /**
     * Creates a RowKeyComponents from a list of components.
     *
     * @param componentsList A List&lt;Object&gt; of the components of the row key.
     * @return a RowKeyComponents
     */
    public static RowKeyComponents fromComponentsList(List<Object> componentsList) {
        return fromComponents(componentsList.toArray());
    }

    /**
     * Gets an EntityId for this RowKeyComponents appropriate to the given table.
     *
     * @param table the Table to generate an EntityId. The table's row key format must be
     *              compatible with the number and type of the components.
     * @return an EntityId appropriate to the table.
     */
    public EntityId getEntityIdForTable(Table table) throws IOException{
        return table.getEntityId(mComponents);
    }

    /**
     * Package-private accessor to retrieve the array of components. Intended for use
     * internally by other members of the package (e.g. EntityIdFactory).
     *
     * @return a list of the components. Do not modify this array.
     */
    public Object[] getComponents() {
        return mComponents;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return Arrays.deepHashCode(mComponents);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (!getClass().equals(obj.getClass())) {
            return false;
        }
        final RowKeyComponents krkc = (RowKeyComponents) obj;

        return Arrays.deepEquals(mComponents, krkc.mComponents);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return Objects.toStringHelper(this)
            .add("Components", Arrays.toString(mComponents))
            .toString();
    }

    /**
     * {@inheritDoc}
     * <p>
     * *Note* the ordering of RowKeyComponents is not representative of the scan order of rows in
     * a Kiji table. Hashing is not taken into account when comparing {@link RowKeyComponents}.
     * <p>
     * Only {@link RowKeyComponents} from the same table may be compared. The compared row key
     * components must contain the same number of components, and the corresponding components in each
     * row key components must be of the same type.
     */
    @Override
    public int compareTo(RowKeyComponents other) {
        Object[] components1 = this.getComponents();
        Object[] components2 = other.getComponents();

        int size1 = components1.length;
        int size2 = components2.length;

        if (size1 != size2) {
            throw new IllegalArgumentException(
                String.format(
                    "Can not compare RowKeyComponents with different numbers of components."
                        + " Components: %s, %s.", this, other));
        }

        // Compare individual components
        for (int i = 0; i < size1; i++) {
            Object a = components1[i];
            Object b = components2[i];
            // null values sort first
            if (a == null && b == null) {
                continue;
            }
            if (a == null) {
                return -1;
            }
            if (b == null) {
                return 1;
            }

            // If both components are non-null, then use natural comparison
            final int comparison;
            try {
                if (a instanceof String) {
                    comparison = ((String) a).compareTo((String) b);
                } else if (a instanceof Integer) {
                    comparison = ((Integer) a).compareTo((Integer) b);
                } else if (a instanceof Long) {
                    comparison = ((Long) a).compareTo((Long) b);
                } else if (a instanceof byte[]) {
                    comparison = UnsignedBytes.lexicographicalComparator().compare((byte[]) a, (byte[]) b);
                } else {
                    throw new IllegalArgumentException(
                        String.format("Unknown Entity Id component type %s.", a.getClass()));
                }
                if (comparison != 0) {
                    return comparison;
                }
            } catch (ClassCastException e) {
                throw new IllegalArgumentException(
                    String.format(
                        "Can not compare RowKeyComponents with different component types."
                            + " Index: %s. Types: %s, %s. Components: %s, %s.",
                        i, a.getClass(), b.getClass(), this, other));
            }
        }
        return 0;
    }


}
